package applet

import (
	"bytes"
	"fmt"
	"math/big"
	"strings"

	"gitee.com/caivega/easm/core"
	"github.com/ethereum/go-ethereum/core/vm"
)

type Label struct {
	offset int
	size   int
	calls  []*Call
}

type Call struct {
	offset int
	label  *Label
}

type Applet struct {
	cb       *core.Buffer
	cfg      *core.Config
	labelMap map[string]*Label
}

func CreateApplet(cfg *core.Config, cb *core.Buffer) *Applet {
	return &Applet{cb, cfg, make(map[string]*Label)}
}

func (at *Applet) Append(ds []byte) *Applet {
	at.cb.Append(ds)
	return at
}

func (at *Applet) AppendCode(op vm.OpCode) *Applet {
	at.cb.Append([]byte{byte(op)})
	return at
}

func (at *Applet) PushCall(name string) *Applet {
	label, ok := at.labelMap[name]
	if !ok {
		label = at.createLabel(name)
	}
	at.cb.Append([]byte{byte(vm.OpCode(int(vm.PUSH0) + label.size))})

	label.calls = append(label.calls, &Call{offset: at.cb.Len(), label: label})
	at.cb.Append(bytes.Repeat([]byte{0x0}, label.size))
	return at
}

func (at *Applet) createLabel(name string) *Label {
	label := at.createLabelWith(name, at.cfg.LabelSize)
	return label
}

func (at *Applet) createLabelWith(name string, size int) *Label {
	label := &Label{at.cb.Len(), size, make([]*Call, 0)}
	at.labelMap[name] = label
	return label
}

func (at *Applet) CreateLabel(name string) *Applet {
	at.CreateLabelWith(name, at.cfg.LabelSize)
	return at
}

func (at *Applet) CreateLabelWith(name string, size int) *Applet {
	label, ok := at.labelMap[name]
	if !ok {
		at.createLabelWith(name, size)
	} else {
		label.offset = at.cb.Len()
	}
	at.cb.Append([]byte{byte(vm.JUMPDEST)})
	return at
}

func (at *Applet) Update() map[int]string {
	indexMap := make(map[int]string)
	for n, label := range at.labelMap {
		indexMap[label.offset] = n

		n := new(big.Int).SetUint64(uint64(label.offset))
		ds := n.Bytes()
		for _, call := range label.calls {
			at.cb.Write(call.offset, ds)
		}
	}
	return indexMap
}

func (at *Applet) Bytes() []byte {
	return at.cb.Bytes()
}

func (at *Applet) Dump() {
	indexMap := at.Update()
	maxLen := 0
	for _, name := range indexMap {
		ll := len(name)
		if ll > maxLen {
			maxLen = ll
		}
	}
	code := at.Bytes()
	core.DumpCodeWith(code, func(p int, lop int, ldata []byte) {
		name, ok := indexMap[p]
		if ok {
			fmt.Printf("%s: %s", name, strings.Repeat(" ", maxLen-len(name)))
		} else {
			fmt.Printf("%s  ", strings.Repeat(" ", maxLen))
		}
	}, func(p int, op int, lop int, ldata []byte) {
		if vm.OpCode(lop).IsPush() && (vm.OpCode(op) == vm.JUMP || vm.OpCode(op) == vm.JUMPI) {
			n := new(big.Int).SetBytes(ldata)
			name, ok := indexMap[int(n.Uint64())]
			if ok {
				fmt.Printf(" %s", name)
			}
		}
	})
}
